/* * Copyright 2008 Eckhart Arnold (eckhart_arnold@hotmail.com). * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of * the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * License for the specific language governing permissions and limitations under * the License. */ package de.eckhartarnold.client; import java.util.HashMap; import java.util.ArrayList; import com.google.gwt.json.client.JSONArray; import com.google.gwt.json.client.JSONException; import com.google.gwt.json.client.JSONObject; import com.google.gwt.json.client.JSONParser; import com.google.gwt.json.client.JSONValue; import com.google.gwt.core.client.GWT; import com.google.gwt.dom.client.Document; import com.google.gwt.dom.client.Element; import com.google.gwt.dom.client.NodeList; import com.google.gwt.event.dom.client.ClickEvent; import com.google.gwt.event.dom.client.ClickHandler; import com.google.gwt.http.client.Request; import com.google.gwt.http.client.RequestBuilder; import com.google.gwt.http.client.RequestCallback; import com.google.gwt.http.client.RequestException; import com.google.gwt.http.client.Response; import com.google.gwt.user.client.Window; import com.google.gwt.user.client.ui.Button; import com.google.gwt.user.client.ui.DialogBox; import com.google.gwt.user.client.ui.HasHorizontalAlignment; import com.google.gwt.user.client.ui.HTML; import com.google.gwt.user.client.ui.VerticalPanel; import com.google.gwt.safehtml.shared.SafeHtml; import com.google.gwt.safehtml.shared.OnlyToBeUsedInGeneratedCodeStringBlessedAsSafeHtml; /** * Reads the information about the image collection from a number of * .json files. * * <p>When the information is ready it can be queried with the * <code>getCaptions</code>, <code>getDirectories</code>, * <code>getImageNames</code>, <code>getImageSizes</code>, * <code>getInfo</code> methods. The information on the * image collection must contained in five files with the following hard * coded names: * <p><ol> * <li>"directories.json" - a list of directories * <li>"filenames.json" - a list of image file names * <li>"captions.json" - a dictionary that associates the image file names * with the captions of the images * <li>"resolutions.json" - Either a list with two dictionaries, where the * defines a certain number "resolution sets" (i.e. * lists of image sizes that are associated with a * name, e.g. "landscape" or "portrait") and * the second associates each image name with the * key of a resolution set <em>or</em> * a dictionary that associates the image file names * directly with list of image sizes, each size * entry corresponding to one directory and * consisting itself of a two entry list containing * the width and height of the image in the * respective directory. * <li>"info.json" - a dictionary with arbitrary additional * information on the image collection. It should * (but need not) contain at least the fields: * "title", "subtitle" and "bottom line" * </ol> * * @author ecki * */ public class ImageCollectionReader implements ImageCollectionInfo { /** * This interface defines a call back function that is called with the * <code>ImageCollectionReader</code> object as parameter. This is * used for calling back into the main program by the time all the * .json files defining the photo album have been read and parsed. * */ public interface ICallback { /** * A call back function called * from the <code>ImageCollectionReader</code> object * @param src the caller object of the call back */ void callback(ImageCollectionReader src); } /** * This interface defines a call back that receives a string message. * It is used for transmitting error messages to clients of class * <code>ImageCollectionReader</code> */ public interface IMessage { /** * A call back function that receives a string message as parameter. * @param msg the message */ void message(String msg); } private interface JSONDelegate { void process(JSONValue json); } /** * This is a helper class that allows displaying an error message in * a dialog so that the user can be notified of any error that occurred * during reading the configuration data from the .json files. * It is in the discretion of the client of class * <code>ImageCollectionReader</code> whether the user is notified or not, * for which purpose, however, this class may be helpful. */ public static class MessageDialog implements IMessage { private DialogBox dialog; /** * Displays a message in a pop up dialog. * * @param msg the message to be displayed. */ public void message(String msg) { dialog = new DialogBox(true); dialog.setText("Error!"); dialog.addStyleName("debugger"); VerticalPanel panel = new VerticalPanel(); panel.setSpacing(4); panel.add(new HTML(msg)); Button button = new Button("close", new ClickHandler() { public void onClick(ClickEvent event) { dialog.hide(); dialog = null; } }); panel.add(button); panel.setCellHorizontalAlignment(button, HasHorizontalAlignment.ALIGN_CENTER); dialog.setWidget(panel); dialog.center(); dialog.show(); } } private class JSONReceiver implements RequestCallback { String url; JSONDelegate task; IMessage errorReporting; JSONReceiver(String url, JSONDelegate task, IMessage error) { this.url = url; this.task = task; this.errorReporting = error; } /** * Checks whether the JSON data is stored * in a (hidden) tag, the id of which must be the file name, * e.g. <div id="resolutions.json" style="display:none">JSON</div> * @return true, if successful */ public boolean extractJSONfromHTML() { JSONValue jsonValue; String tagId = url.substring(url.lastIndexOf('/')+1); Element dataTag = Document.get().getElementById(tagId); if (dataTag != null) { jsonValue = JSONParser.parseStrict(dataTag.getInnerHTML()); task.process(jsonValue); return true; } else { String location = Window.Location.getHref(); /* if (!location.contains("GWTPhotoAlbum_fat.html")) { String target = "GWTPhotoAlbum_fat.html"; int i = location.lastIndexOf("#"); if (i >= 0) { target += location.substring(i); } redirect(GWT.getHostPageBaseURL()+target); } */ return false; } } /* (non-Javadoc) * @see com.google.gwt.http.client.RequestCallback#onError(com.google.gwt.http.client.Request, java.lang.Throwable) */ public void onError(Request request, Throwable exception) { // errorReporting.message("Couldn't retrieve JSON: " + url + // "<br />" + exception.getMessage()); } /* (non-Javadoc) * @see com.google.gwt.http.client.RequestCallback#onResponseReceived(com.google.gwt.http.client.Request, com.google.gwt.http.client.Response) */ public void onResponseReceived(Request request, Response response) { JSONValue jsonValue; int statusCode = response.getStatusCode(); try { if (statusCode == Response.SC_OK) { // SC_OK == 200 !? jsonValue = JSONParser.parseStrict(response.getText()); task.process(jsonValue); GWT.log("JSON read: "+url); } else { // if no file is found, check whether the JSON data is stored // in a (hidden) tag of the html master file. if (!extractJSONfromHTML()) { errorReporting.message("Couldn't retrieve JSON from HTML: " + url + "<br /> after previous error " + statusCode + ": " + response.getStatusText()); } GWT.log("JSON extracted from html: " + url.substring(url.lastIndexOf('/')+1)); } } catch (JSONException e) { errorReporting.message("Could not parse JSON: " + url + "<br />" + e.getMessage()); } } } /** * The name of the html meta-tag that contains an alternative file name * for file "info.json". This meta-tag allows to switch the info.json, * file that control the appearance of the photo album, from within the * the html page that contains the photo album script. */ public static final String METANAME_INFO = "info"; /** An instance of the inner <code>MessageDialog</code> class. */ public static final IMessage ERROR_DIALOG = new MessageDialog(); /** An instanca of interface <code>IMessage</code> that silently * passes over the message */ public static final IMessage ERROR_SILENT = new IMessage() { public void message(String msg) { } }; private static native void redirect(String url)/*-{ $wnd.location = url; }-*/; private SafeHtml[] captions; private HashMap<String, String> captionDictionary; private String[] directories; private boolean finished = false; private String[] imageNames; private HashMap<String, int[][]> imageSizes; private HashMap<String, String> info; private String infoFileName; /** * Reads the information about the image collection from several json files * contained in the <code>baseURL</code> directory. * When the json files have finished loading <code>readyReport</code> is * issued. If any error occurs it is reported via <code>errorReport</code>. * For error reporting the class <code>ImageCollectionInfo</code> offers * two stock objects: <code>ERROR_DIALOG</code> and <code>ERROR_SILET</code>, * the first of which reports the error in a pop up dialog, while the other * simply ignores the error (dangerous!). * * @param baseURL the base URL of the image collection * @param readyReport the callback that is to be issued when loading the * information about the image collection is ready. * @param errorReport the callback for reporting errors. */ public ImageCollectionReader(String baseURL, ICallback readyReport, IMessage errorReport) { // Element info = DOM.getElementById("info"); NodeList<Element> metaTags = Document.get().getElementsByTagName("meta"); this.infoFileName = "info.json"; int length = metaTags.getLength(); for (int i = 0; i < length; i++) { Element item = metaTags.getItem(i); if (item.getAttribute("name").equalsIgnoreCase(METANAME_INFO)) { this.infoFileName = item.getAttribute("content"); break; } } retrieveSequentially(baseURL, readyReport, errorReport); } /* (non-Javadoc) * @see de.eckhartarnold.client.ImageCollectionInterface#getCaptions() */ public SafeHtml[] getCaptions() { assert captions != null : "captions not loaded yet!"; return captions; } /* (non-Javadoc) * @see de.eckhartarnold.client.ImageCollectionInterface#getDirectories() */ public String[] getDirectories() { assert directories != null : "directory information not loaded yet!"; return directories; } /* (non-Javadoc) * @see de.eckhartarnold.client.ImageCollectionInterface#getImageNames() */ public String[] getImageNames() { assert imageNames != null: "image names not loaded yet!"; return imageNames; } /* (non-Javadoc) * @see de.eckhartarnold.client.ImageCollectionInterface#getImageSizes() */ public HashMap<String, int[][]> getImageSizes() { assert imageSizes != null: "information about image sizes not loaded yet!"; return imageSizes; } /* (non-Javadoc) * @see de.eckhartarnold.client.ImageCollectionInterface#getInfo() */ public HashMap<String, String> getInfo() { if (info == null) GWT.log("Info not available!!!"); assert info != null: "information on the image collection not yet loaed!"; return info; } public boolean hasCaptions() { return captionDictionary.isEmpty(); } /** * Returns true, if loading the information about the image collection has * finished and it is ready to be queried. * * @return true, if the information about the image collection is ready */ public boolean isReady() { if (captionDictionary != null && directories != null && imageNames != null && imageSizes != null && info != null) { if (captions == null) { captions = new SafeHtml[imageNames.length]; for (int i = 0; i < imageNames.length; i++) { if (captionDictionary.containsKey(imageNames[i])) captions[i] = ExtendedHtmlSanitizer.sanitizeHTML( captionDictionary.get(imageNames[i])); else captions[i] = new OnlyToBeUsedInGeneratedCodeStringBlessedAsSafeHtml(""); } } return true; } else return false; } private HashMap<String, int[][]> interpretSizes(JSONValue json) throws JSONException { HashMap<String, int[][]> resolutions = new HashMap<String, int[][]>(); HashMap<String, int[][]> sizes = new HashMap<String, int[][]>(); JSONObject dict = json.isObject(); JSONArray array = json.isArray(); if (array != null) { JSONObject resDict = array.get(0).isObject(); for (String key: resDict.keySet()) { JSONArray resSet = resDict.get(key).isArray(); resolutions.put(key, interpretResolutionsArray(resSet)); } dict = array.get(1).isObject(); } for (String key: dict.keySet()) { // JSONArray list = dict.get(key).isArray(); // resolutions.clear(); // for (int i = 0; i < list.size(); i++ ) { // JSONArray xy = list.get(i).isArray(); // int res[] = new int[2]; // res[0] = (int) xy.get(0).isNumber().doubleValue(); // res[1] = (int) xy.get(1).isNumber().doubleValue(); // resolutions.add(res); // } // sizes.put(key, resolutions.toArray(new int[resolutions.size()][])); JSONValue value = dict.get(key); array = value.isArray(); if (array != null) { sizes.put(key, interpretResolutionsArray(array)); } else { sizes.put(key, resolutions.get(value.isString().stringValue())); } } return sizes; } private String[] interpretStringArray(JSONValue json) throws JSONException { JSONArray array = json.isArray(); ArrayList<String> stringList = new ArrayList<String>(); for (int i = 0; i < array.size(); i++) { stringList.add(array.get(i).isString().stringValue()); } return stringList.toArray(new String[stringList.size()]); } private HashMap<String, String> interpretStringDictionary(JSONValue json) throws JSONException { JSONObject dict = json.isObject(); HashMap<String, String> stringDict = new HashMap<String, String>(); for (String key: dict.keySet()) { stringDict.put(key, dict.get(key).isString().stringValue()); } return stringDict; } private int[][] interpretResolutionsArray(JSONArray array) { ArrayList<int[]> resolutions = new ArrayList<int[]>(); for (int i = 0; i < array.size(); i++ ) { JSONArray xy = array.get(i).isArray(); int res[] = new int[2]; res[0] = (int) xy.get(0).isNumber().doubleValue(); res[1] = (int) xy.get(1).isNumber().doubleValue(); resolutions.add(res); } return resolutions.toArray(new int[resolutions.size()][]); } private void readJSON(String url, JSONDelegate task, IMessage error) { RequestBuilder builder = new RequestBuilder(RequestBuilder.GET, url); JSONReceiver receiver = new JSONReceiver(url, task, error); if (!receiver.extractJSONfromHTML()) { try { builder.sendRequest(null, receiver); } catch (RequestException e) { error.message("Couldn't retrieve JSON: " + url + "<br />" + e.getMessage()); } } } private void retrieveSequentially(String baseURL, ICallback readyReport, IMessage errorReport) { final ICallback ready = readyReport; final IMessage error = errorReport; final ImageCollectionReader src = this; final String url = baseURL; if (directories == null) { readJSON(baseURL+"/directories.json", new JSONDelegate() { public void process(JSONValue json) { directories = interpretStringArray(json); for (int i = 0; i < directories.length; i++) directories[i] = url + "/" + directories[i]; if (isReady() && !finished) { finished = true; ready.callback(src); } else retrieveSequentially(url, ready, error); } }, errorReport); } else if (imageNames == null) { readJSON(baseURL+"/filenames.json", new JSONDelegate() { public void process(JSONValue json) { imageNames = interpretStringArray(json); if (isReady() && !finished) { finished = true; ready.callback(src); } else retrieveSequentially(url, ready, error); } }, errorReport); } else if (captionDictionary == null) { readJSON(baseURL+"/captions.json", new JSONDelegate() { public void process(JSONValue json) { captionDictionary = interpretStringDictionary(json); if (isReady() && !finished) { finished = true; ready.callback(src); } else retrieveSequentially(url, ready, error); } }, errorReport); } else if (imageSizes == null) { readJSON(baseURL+"/resolutions.json", new JSONDelegate() { public void process(JSONValue json) { imageSizes = interpretSizes(json); if (isReady() && !finished) { finished = true; ready.callback(src); } else retrieveSequentially(url, ready, error); } }, errorReport); } else if (info == null) { readJSON(baseURL+"/" + infoFileName, new JSONDelegate() { public void process(JSONValue json) { GWT.log(json.toString()); info = interpretStringDictionary(json); if (isReady() && !finished) { finished = true; ready.callback(src); } else retrieveSequentially(url, ready, error); } }, errorReport); } } // private HashMap<String, SafeHtml> sanitzeDict(HashMap<String, String> dict) { // HashMap<String, SafeHtml> sanitized = new HashMap<String, SafeHtml>(); // for (String key: dict.keySet()) { // sanitized.put(key, ExtendedHtmlSanitizer.sanitizeHTML(dict.get(key))); // } // return sanitized; // } }